﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Aoite.Reflection
{
    internal static class MemberFilter
    {
        public static bool IsReservedName(this string name)
        {
            name = name.ToLowerInvariant();
            return name == ".ctor" || name == ".cctor";
        }

        public static string TrimExplicitlyImplementedName(this string name)
        {
            int index = name.IsReservedName() ? -1 : name.LastIndexOf('.') + 1;
            return index > 0 ? name.Substring(index) : name;
        }

        /// <summary>
        /// This method applies name filtering to a set of members.
        /// </summary>
        public static IList<T> Filter<T>(this IList<T> members, Flags bindingFlags, string[] names)
            where T : MemberInfo
        {
            var result = new List<T>(members.Count);
            bool ignoreCase = bindingFlags.IsSet(Flags.IgnoreCase);
            bool isPartial = bindingFlags.IsSet(Flags.PartialNameMatch);
            bool trimExplicit = bindingFlags.IsSet(Flags.TrimExplicitlyImplemented);

            for(int i = 0; i < members.Count; i++)
            {
                var member = members[i];
                var memberName = trimExplicit ? member.Name.TrimExplicitlyImplementedName() : member.Name;
                for(int j = 0; j < names.Length; j++)
                {
                    var name = names[j];
                    var comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
                    bool match = isPartial ? memberName.Contains(name) : memberName.Equals(name, comparison);
                    if(match)
                    {
                        result.Add(member);
                        break;
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// This method applies type parameter type filtering to a set of methods.
        /// </summary>
        public static IList<T> Filter<T>(this IList<T> methods, Type[] genericTypes)
            where T : MethodBase
        {
            var result = new List<T>(methods.Count);
            for(int i = 0; i < methods.Count; i++)
            {
                var method = methods[i];
                if(method.ContainsGenericParameters)
                {
                    var genericArgs = method.GetGenericArguments();
                    if(genericArgs.Length != genericTypes.Length)
                        continue;
                    result.Add(method);
                }
            }
            return result;
        }

        /// <summary>
        /// This method applies method parameter type filtering to a set of methods.
        /// </summary>
        public static IList<T> Filter<T>(this IList<T> methods, Flags bindingFlags, Type[] paramTypes)
            where T : MethodBase
        {
            var result = new List<T>(methods.Count);

            bool exact = bindingFlags.IsSet(Flags.ExactBinding);
            for(int i = 0; i < methods.Count; i++)
            {
                var method = methods[i];
                // verify parameters
                var parameters = method.GetParameters();
                if(parameters.Length != paramTypes.Length)
                {
                    continue;
                }
                // verify parameter type compatibility
                bool match = true;
                for(int j = 0; j < paramTypes.Length; j++)
                {
                    var type = paramTypes[j];
                    var parameter = parameters[j];
                    Type parameterType = parameter.ParameterType;
                    bool ignoreParameterModifiers = !exact;
                    if(ignoreParameterModifiers && parameterType.IsByRef)
                    {
                        string name = parameterType.FullName;
                        parameterType = Type.GetType(name.Substring(0, name.Length - 1)) ?? parameterType;
                    }
                    match &= parameterType.IsGenericParameter || parameterType.ContainsGenericParameters || (exact ? type == parameterType : parameterType.IsAssignableFrom(type));
                    if(!match)
                    {
                        break;
                    }
                }
                if(match)
                {
                    result.Add(method);
                }
            }
            return result;
        }

        /// <summary>
        /// This method applies member type filtering to a set of members.
        /// </summary>
        public static IList<T> Filter<T>(this IList<T> members, Flags bindingFlags, MemberTypes memberTypes)
            where T : MemberInfo
        {
            var result = new List<T>(members.Count);

            for(int i = 0; i < members.Count; i++)
            {
                var member = members[i];
                bool match = (member.MemberType & memberTypes) == member.MemberType;
                if(!match)
                {
                    continue;
                }
                result.Add(member);
            }
            return result;
        }

        /// <summary>
        /// This method applies flags-based filtering to a set of members.
        /// </summary>
        public static IList<T> Filter<T>(this IList<T> members, Flags bindingFlags) where T : MemberInfo
        {
            var result = new List<T>(members.Count);
            var properties = new List<string>(members.Count);

            bool excludeHidden = bindingFlags.IsSet(Flags.ExcludeHiddenMembers);
            bool excludeBacking = bindingFlags.IsSet(Flags.ExcludeBackingMembers);
            bool excludeExplicit = bindingFlags.IsSet(Flags.ExcludeExplicitlyImplemented);

            for(int i = 0; i < members.Count; i++)
            {
                var member = members[i];
                bool exclude = false;
                if(excludeHidden)
                {
                    var method = member as MethodBase;
                    // filter out anything but methods/constructors based on their name only
                    exclude |= method == null && result.Any(m => m.Name == member.Name);
                    // filter out methods that do not have a unique signature (this prevents overloads from being excluded by the ExcludeHiddenMembers flag)
                    exclude |= method != null && result.Where(m => m is MethodBase).Cast<MethodBase>().Any(m => m.Name == member.Name && m.HasParameterSignature(method.GetParameters()));
                }
                if(!exclude && excludeBacking)
                {
                    exclude |= member is FieldInfo && member.Name[0] == '<';
                    var method = member as MethodInfo;
                    if(method != null)
                    {
                        // filter out property backing methods
                        exclude |= member.Name.Length > 4 && member.Name.Substring(1, 3) == "et_";
                        // filter out base implementations when an overrride exists
                        exclude |= result.ContainsOverride(method);
                    }
                    var property = member as PropertyInfo;
                    if(property != null)
                    {
                        MethodInfo propertyGetter = property.GetGetMethod(true);
                        exclude |= propertyGetter.IsVirtual && properties.Contains(property.Name);
                        if(!exclude)
                        {
                            properties.Add(property.Name);
                        }
                    }
                }
                exclude |= excludeExplicit && member.Name.Contains(".") && !member.Name.IsReservedName();
                if(exclude)
                {
                    continue;
                }
                result.Add(member);
            }
            return result;
        }

        private static bool ContainsOverride<T>(this IList<T> candidates, MethodInfo method) where T : MemberInfo
        {
            if(!method.IsVirtual)
                return false;
            var parameters = method.Parameters();
            for(int i = 0; i < candidates.Count; i++)
            {
                MethodInfo candidate = candidates[i] as MethodInfo;
                if(candidate == null || !candidate.IsVirtual || method.Name != candidate.Name)
                {
                    continue;
                }
                if(parameters.Select(p => p.ParameterType).SequenceEqual(candidate.Parameters().Select(p => p.ParameterType)))
                {
                    return true;
                }
            }
            return false;
        }
    }
}